﻿namespace DatabaseDoc.Parameters
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    public class ParameterValidation
    {        
        private ParameterConfiguration Configuration { get; set; }

        public ParameterValidation(ParameterConfiguration configuration)
        {
            if (configuration == null)
            {
                throw new ArgumentNullException("configuration");
            }

            this.Configuration = configuration;
        }

        public bool IsHelp(string[] arguments)
        {
            if (arguments == null || arguments.Count() == 0)
            {
                return false;
            }

            var helpParam = string.Format("{0}{1}", this.Configuration.ParamPrefix, this.Configuration.HelpParameterName);
            return arguments.Any(a => string.Compare(a, helpParam, true) == 0);
        }

        public Parameters GenerateParameters(string[] arguments)
        {
            if (arguments == null || arguments.Length == 0)
            {
                return null;
            }
            
            var returnParameters = new Parameters();            

            this.CheckRequiredParametersPresent(arguments);

            foreach (var argument in arguments)
            {
                if (string.IsNullOrWhiteSpace(argument))
                {
                    throw new InvalidParametersException("Argument has no value");
                }

                if (!this.CheckValidArgumentFormat(argument))
                {
                    throw new InvalidParametersException(string.Format("Argument {0} is not in the correct format", argument));
                }

                if (!this.CheckArgumentExists(argument))
                {
                    throw new InvalidParametersException(string.Format("Argument {0} is not a valid argument", argument));
                }

                var parameter = this.GetParameter(argument);

                if (!this.CheckValidOptions(parameter))
                {
                    throw new InvalidParametersException(string.Format("Argument {0} has invalid options specified", argument));
                }

                returnParameters.Add(parameter);
            }

            this.CheckParameterDependencies(returnParameters);

            this.CheckParameterAcceptances(returnParameters);

            return returnParameters;
        }

        private void CheckParameterAcceptances(IEnumerable<Parameter> parametersToCheck)
        {
            foreach(var param in parametersToCheck)
            {
                var param1 = param;

                if (this.Configuration.Parameters.ContainsKey(param1.Name))
                {
                    var acceptance = this.Configuration.Parameters[param1.Name].ParameterAcceptance;

                    if (acceptance != null)
                    {
                        acceptance(param1, parametersToCheck.Except(new List<Parameter> { param1 }));                        
                    }
                }                
            }
        }

        private void CheckParameterDependencies(IEnumerable<Parameter> parametersToCheck)
        {
            foreach (var param in parametersToCheck)
            {
                var param1 = param;

                // Get a collection of the parameters dependencies
                var dependencyKvps = this.Configuration.Dependencies.Where(d => param1.Matches(d.Key.ParameterName));

                foreach (var dependencyKvp in dependencyKvps)
                {
                    var depend = dependencyKvp.Key;
                    var dependRequirement = dependencyKvp.Value;

                    if (dependRequirement == DependencyRequirement.Required)
                    {
                        if (!parametersToCheck.Any(p => p.Matches(depend.DependencyParameterName)))
                        {
                            throw new InvalidParametersException(string.Format("Parameter {0} has a dependency on parameter {1} which has not been specified", param1.Name, depend.DependencyParameterName));
                        }
                    }

                    if (dependRequirement == DependencyRequirement.NotValid)
                    {
                        if (parametersToCheck.Any(p => p.Matches(depend.DependencyParameterName)))
                        {
                            throw new InvalidParametersException(string.Format("Parameter {0} is not valid when the parameter {1} is specified", depend.DependencyParameterName, param1.Name));
                        }
                    }
                }
            }
        }

        private void CheckRequiredParametersPresent(string[] arguments)
        {
            if (arguments == null || arguments.Length == 0)
            {
                return;
            }

            foreach (var kvp in this.Configuration.Parameters)
            {
                var parameterName = kvp.Key;
                var parameterRequirements = kvp.Value;

                if (parameterRequirements.ParameterRequirement != ParameterRequirement.Required)
                {
                    continue;
                }

                if (!arguments.Any(a => string.Compare(this.GetParameterName(a), parameterName, true) == 0))
                {
                    throw new InvalidParametersException(
                        string.Format("A required parameter {0} has not been specified", parameterName));
                }
            }
        }

        private Parameter GetParameter(string argument)
        {
            var parameter = new Parameter
            {
                Name = this.GetParameterName(argument),
                Value = this.GetParameterValue(argument),
                Options = this.GetParameterOptions(argument)
            };

            return parameter;
        }

        private string GetParameterName(string argument)
        {
            if (string.IsNullOrWhiteSpace(argument))
            {
                return null;
            }

            var paramPrefixIndex = argument.IndexOf(this.Configuration.ParamPrefix);
            var optionPrefixIndex = argument.IndexOf(this.Configuration.OptionPrefix);
            var startIndex = paramPrefixIndex == -1 ? 0 : paramPrefixIndex + 1;
            var endIndex = optionPrefixIndex == -1 ? argument.Length : optionPrefixIndex;

            var parameterName = argument.Substring(startIndex, endIndex - startIndex);
            return string.IsNullOrWhiteSpace(parameterName) ? null : parameterName;
        }

        private string GetParameterValue(string argument)
        {
            if (string.IsNullOrWhiteSpace(argument))
            {
                return null;
            }

            var optionPrefixIndex = argument.IndexOf(this.Configuration.OptionPrefix);
            if (optionPrefixIndex == -1)
            {
                return null;
            }

            var parameterValue = argument.Substring(optionPrefixIndex + 1);
            return parameterValue;
        }

        private IEnumerable<Option> GetParameterOptions(string argument)
        {
            var returnOptions = new List<Option>();

            var value = this.GetParameterValue(argument);

            if (string.IsNullOrWhiteSpace(value))
            {
                return returnOptions;
            }

            var options = value.Split(new[] { this.Configuration.OptionSeparator }, StringSplitOptions.RemoveEmptyEntries);

            foreach (var option in options)
            {
                if (!option.Contains(this.Configuration.OptionAssigner))
                {
                    returnOptions.Add(new Option { Name = option, Value = option });
                    continue;
                }

                var optionValues = option.Split(new[] { this.Configuration.OptionAssigner }, StringSplitOptions.RemoveEmptyEntries);
                if (optionValues.Length != 2)
                {
                    throw new InvalidParametersException(string.Format("Argument {0} has an invalid option assignment", argument));
                }

                returnOptions.Add(new Option { Name = optionValues[0], Value = optionValues[1] });
            }

            return returnOptions;
        }

        private bool CheckValidOptions(Parameter parameter)
        {
            if (!this.Configuration.Parameters.ContainsKey(parameter.Name))
            {
                throw new InvalidParametersException(string.Format("Parameter {0} isn't a valid parameter", parameter.Name));
            }

            var param = this.Configuration.Parameters[parameter.Name];
            if (param.Options.Requirement == OptionsRequirement.Required)
            {
                if (!parameter.HasOptions)
                {
                    throw new InvalidParametersException(
                        string.Format("Parameter {0} has required options that aren't specified", parameter.Name));
                }

                if (param.Options.HasSingleOption && !parameter.HasSingleOption)
                {
                    throw new InvalidParametersException(string.Format("Parameter {0} has a single option, but multiple options were specified", parameter.Name));
                }

                if (param.Options.HasMultipleOptions && !parameter.HasMultipleOptions)
                {
                    throw new InvalidParametersException(string.Format("Parameter {0} has multiple options, but no multiple options were specified", parameter.Name));
                }

                if (param.Options.HasSingleOption)
                {
                    var option = parameter.Options.FirstOrDefault();
                    if (option == null || string.Compare(option.Name, option.Value, true) != 0)
                    {
                        throw new InvalidParametersException(string.Format("Parameter {0} has a single option, but there is an invalid value for that option", parameter.Name));
                    }
                }

                //TODO: validate multiple options, should be able to specify option types (single and multiple), e.g. int, double, regex etc
                // and also if multiple, what the option names are, and they valid data types
            }

            if (param.Options.Requirement == OptionsRequirement.NotValid && parameter.HasOptions)
            {
                throw new InvalidParametersException(string.Format("Parameter {0} has no options, but options are specified", parameter.Name));
            }
                
            return true;
        }

        private bool CheckArgumentExists(string argument)
        {
            var parameterName = this.GetParameterName(argument);
            return this.Configuration.Parameters.ContainsKey(parameterName);
        }

        private bool CheckValidArgumentFormat(string argument)
        {
            return argument.StartsWith(this.Configuration.ParamPrefix);
        }

        public Parameters GenerateParameters(IEnumerable<string> arguments)
        {            
            return this.GenerateParameters(arguments == null ? null : arguments.ToArray());
        }
    }
}